package com.basker.pisces.redis.config;

import java.net.URI;
import java.net.URISyntaxException;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;

import org.redisson.config.ClusterServersConfig;
import org.redisson.config.Config;
import org.redisson.config.SentinelServersConfig;
import org.redisson.config.SingleServerConfig;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.data.redis.RedisProperties;
import org.springframework.boot.autoconfigure.data.redis.RedisProperties.Cluster;
import org.springframework.boot.autoconfigure.data.redis.RedisProperties.Sentinel;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.NamedNode;
import org.springframework.data.redis.connection.RedisClusterConfiguration;
import org.springframework.data.redis.connection.RedisNode;
import org.springframework.data.redis.connection.RedisPassword;
import org.springframework.data.redis.connection.RedisSentinelConfiguration;
import org.springframework.data.redis.connection.RedisStandaloneConfiguration;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;

import com.basker.pisces.redis.utils.RedisUtils;

/**
 * 利用springboot的redis属性，创建Redisson框架的配置类
 *
 * @author hangwen
 */

@Configuration
@ConditionalOnClass(Config.class)
public class RedissonConnectionConfiguration {

    private RedisProperties properties;
    private ObjectProvider<RedisSentinelConfiguration> sentinelConfigurationProvider;
    private ObjectProvider<RedisClusterConfiguration> clusterConfigurationProvider;

    public RedissonConnectionConfiguration(RedisProperties properties,
                                           ObjectProvider<RedisSentinelConfiguration> sentinelConfigurationProvider,
                                           ObjectProvider<RedisClusterConfiguration> clusterConfigurationProvider) {
        this.properties = properties;
        this.sentinelConfigurationProvider = sentinelConfigurationProvider;
        this.clusterConfigurationProvider = clusterConfigurationProvider;
    }

    @Bean
    public Config redissonConfig() {
        RedisSentinelConfiguration sentinelConfiguration = this.tryGetRedisSentinelConfiguration();
        if (sentinelConfiguration != null) {
            return this.createRedissonSentinelConfig(sentinelConfiguration);
        }

        RedisClusterConfiguration clusterConfiguration = this.tryGetRedisClusterConfiguration();
        if (clusterConfiguration != null) {
            return this.createRedissonClusterConfig(clusterConfiguration);
        }

        RedisStandaloneConfiguration standaloneConfiguration = this.getStandaloneConfig();
        return this.createRedissonStandaloneConfig(standaloneConfiguration);
    }

    private RedisClusterConfiguration createRedisClusterConfiguration() {
        Cluster cluster = this.properties.getCluster();
        if (cluster == null) {
            return null;
        }

        RedisClusterConfiguration config = new RedisClusterConfiguration(cluster.getNodes());

        Integer maxRedirects = cluster.getMaxRedirects();
        if (maxRedirects != null) {
            config.setMaxRedirects(maxRedirects);
        }

        String password = this.properties.getPassword();
        if (password != null) {
            config.setPassword(RedisPassword.of(password));
        }

        return config;
    }

    private RedisSentinelConfiguration createRedisSentinelConfiguration() {
        Sentinel sentinel = this.properties.getSentinel();
        if (sentinel == null) {
            return null;
        }

        RedisSentinelConfiguration config = new RedisSentinelConfiguration();

        config.master(sentinel.getMaster());
        config.setSentinels(createSentinels(sentinel));

        String password = this.properties.getPassword();
        if (password != null) {
            config.setPassword(RedisPassword.of(password));
        }

        config.setDatabase(this.properties.getDatabase());

        return config;
    }

    private Config createRedissonClusterConfig(RedisClusterConfiguration redisClusterConfiguration) {
        Set<RedisNode> clusterNodes = redisClusterConfiguration.getClusterNodes();
        String[] addresses = clusterNodes.stream().map(node -> RedisUtils.resolveUrl(node))
                .toArray(len -> new String[len]);

        Config config = new Config();
        ClusterServersConfig clusterServersConfig = config.useClusterServers().addNodeAddress(addresses);

        Duration timeout = this.properties.getTimeout();
        if (timeout != null) {
            clusterServersConfig.setTimeout((int) timeout.toMillis());
        }

        return config;
    }

    private Config createRedissonSentinelConfig(RedisSentinelConfiguration redisSentinelConfiguration) {
        NamedNode master = redisSentinelConfiguration.getMaster();
        Set<RedisNode> sentinels = redisSentinelConfiguration.getSentinels();
        String[] addresses = sentinels.stream().map(node -> RedisUtils.resolveUrl(node))
                .toArray(len -> new String[len]);

        Config config = new Config();
        SentinelServersConfig sentinelServersConfig = config.useSentinelServers().setMasterName(master.getName())
                .setDatabase(redisSentinelConfiguration.getDatabase()).addSentinelAddress(addresses);

        Duration timeout = this.properties.getTimeout();
        if (timeout != null) {
            sentinelServersConfig.setTimeout((int) timeout.toMillis());
        }

        return config;
    }

    private Config createRedissonStandaloneConfig(RedisStandaloneConfiguration redisStandaloneConfiguration) {
        RedisNode node = new RedisNode(redisStandaloneConfiguration.getHostName(),
                redisStandaloneConfiguration.getPort());

        Config config = new Config();
        SingleServerConfig singleServerConfig = config.useSingleServer().setAddress(RedisUtils.resolveUrl(node))
                .setDatabase(redisStandaloneConfiguration.getDatabase());

        Duration timeout = this.properties.getTimeout();
        if (timeout != null) {
            singleServerConfig.setTimeout((int) timeout.toMillis());
        }

        return config;
    }

    private List<RedisNode> createSentinels(RedisProperties.Sentinel sentinel) {
        List<RedisNode> nodes = new ArrayList<>();

        for (String node : sentinel.getNodes()) {
            try {
                String[] parts = StringUtils.split(node, ":");
                Assert.state(parts.length == 2, "Must be defined as 'host:port'");

                nodes.add(new RedisNode(parts[0], Integer.valueOf(parts[1])));
            } catch (RuntimeException ex) {
                throw new IllegalStateException("Invalid redis sentinel " + "property '" + node + "'", ex);
            }
        }

        return nodes;
    }

    private RedisStandaloneConfiguration getStandaloneConfig() {
        RedisStandaloneConfiguration config = new RedisStandaloneConfiguration();

        if (StringUtils.hasText(this.properties.getUrl())) {
            ConnectionInfo connectionInfo = parseUrl(this.properties.getUrl());
            config.setHostName(connectionInfo.getHostName());
            config.setPort(connectionInfo.getPort());
            config.setPassword(RedisPassword.of(connectionInfo.getPassword()));
        } else {
            config.setHostName(this.properties.getHost());
            config.setPort(this.properties.getPort());
            config.setPassword(RedisPassword.of(this.properties.getPassword()));
        }

        config.setDatabase(this.properties.getDatabase());

        return config;
    }

    private ConnectionInfo parseUrl(String url) {
        try {
            URI uri = new URI(url);
            // boolean useSsl = url.startsWith("rediss://");
            String password = null;
            if (uri.getUserInfo() != null) {
                password = uri.getUserInfo();
                int index = password.indexOf(':');
                if (index >= 0) {
                    password = password.substring(index + 1);
                }
            }
            return new ConnectionInfo(uri, password);
            // return new ConnectionInfo(uri, useSsl, password);
        } catch (URISyntaxException ex) {
            throw new IllegalArgumentException("Malformed url '" + url + "'", ex);
        }
    }

    private RedisClusterConfiguration tryGetRedisClusterConfiguration() {
        RedisClusterConfiguration redisClusterConfiguration = clusterConfigurationProvider.getIfAvailable();
        if (redisClusterConfiguration == null) {
            redisClusterConfiguration = createRedisClusterConfiguration();
        }

        return redisClusterConfiguration;
    }

    private RedisSentinelConfiguration tryGetRedisSentinelConfiguration() {
        RedisSentinelConfiguration redisSentinelConfiguration = sentinelConfigurationProvider.getIfAvailable();
        if (redisSentinelConfiguration == null) {
            redisSentinelConfiguration = createRedisSentinelConfiguration();
        }

        return redisSentinelConfiguration;
    }

    private static class ConnectionInfo {

        private final URI uri;

        private final String password;

        // public ConnectionInfo(URI uri, boolean useSsl, String password) {
        public ConnectionInfo(URI uri, String password) {
            this.uri = uri;
            // this.useSsl = useSsl;
            this.password = password;
        }

        public String getHostName() {
            return this.uri.getHost();
        }

        public String getPassword() {
            return this.password;
        }

        public int getPort() {
            return this.uri.getPort();
        }

    }

}
